# Decision Making in Intelligent Systems
# Assignment 1 - One Play Poker
# Chaum Bastiaan [5742889] & Kai Krabben [5743036]

import random, copy
from itertools import izip
argmax = lambda array: max(izip(array, xrange(len(array))))[1]

# Calculate policy or use saved one?
calcPolicy = True
policyKFile = "optimalpolicy-keys3.txt"
policyVFile = "optimalpolicy-values3.txt"

# Set default values and suit for the deck. Deck will consist of all possible value/suit combinations
defValues = ['Jack', 'Queen', 'King', 'Ace']
defSuits = ['Spades', 'Hearts', 'Clubs', 'Diamonds']


def newDeck():
    # create a new deck as list of Card object
    deck = []
    for value in defValues:
        for suit in defSuits:
            deck.append(Card(value, suit))
    return deck

class Card(object):
    # Card object represents a single card in the poker game
    def __init__(self, value, suit):
        self.value = value
        self.suit = suit

    def __str__(self):
        # convert Card to string for screen output
        return self.value + ' of ' + self.suit

    def getFromString(string):
        # convert string representation back to value & suit
        v, s = string.split(" of ")
        self.value = v
        self.suit = s

    def compare(self, card):
        # check if this object represents the same card as another Card object
        if str(self) == str(card):
            return True
        else:
            return False

class State(object):
    # State object represents a state in the poker game
    def __init__(self):
        self.player = []        # player's cards
        self.house = []         # house's cards
        self.deck = newDeck()   # list of cards in the deck
        self.pot = 2            # pot size in dollars

    def Set(self, player=None, house=None, deck=None, pot=None):
        # set values for player, house, deck and pot
        if not player == None:
            self.player = player
        if not house == None:
            self.house = house
        if not deck == None:
            self.deck = deck
        if not pot == None:
            self.pot = pot

    def getFromString(string):
        # convert string representation back to value & suit
        p, h, po = string.split("/")
        d = newDeck()
        for c in p:
            deckRemove(d,c)
        for c in h:
            deckRemove(d,c)
        self.player = p
        self.house = h
        self.deck = d
        self.pot = po
                       
        
    def show(self):
        # print the current state to the screen
        pCards = []
        hCards = []
        for card in self.player:
            pCards.append(str(card))
        for card in self.house:
            hCards.append(str(card))

        print "====================================================" 
        print "Player cards  : ", pCards
        print "House cards   : ", hCards
        print "Pot size      : ", self.pot
        print "====================================================" 

    def drawCard(self):
        # draw a random card and update the state
        card = self.deck[random.randint(0, (len(self.deck)-1))]
        
        if len(self.player) == len(self.house):
               self.player.append(card)
        else:
               self.house.append(card)
        
        self.deck.remove(card)

    def __str__(self):
        # convert State to string for screen output
        pCards = []
        hCards = []
        for card in self.player:
            pCards.append(str(card))
        for card in self.house:
            hCards.append(str(card))
        
        return "%s/%s/%d" % (".".join(pCards), ".".join(hCards), self.pot)
    
    def simple(self):
        # return a simple list version of the current state
        return [self.player, self.house, self.deck, self.pot]

    def gameOver(self):
        # check if the game is over
        return len(self.player) == 2 and len(self.house) == 2

def toKey(state):
    # return a comparable ID for this state
    if state.__class__.__name__ == 'State':     # full state was given
        sortedPlayer = sortedHand(state.player)
        sortedHouse = sortedHand(state.house)
        pot = state.pot
        
    if state.__class__.__name__ == 'list':      # simple state was given
        sortedPlayer = sortedHand(state[0])
        sortedHouse = sortedHand(state[1])
        pot = state[3]

    return (sortedPlayer, sortedHouse, pot)

def sortedHand(hand):
    newHand = sorted([str(card) for card in hand])
    
    return tuple(newHand)

def newGame(policy, printflag = False):
    # start a new game of poker
    if printflag:
        print "Let's play poker!\n"
        print "Player bets 1 to start the game"
    s0 = State()
    if printflag:
        s0.show()

    return playRound(s0, policy, printflag)
    

def playRound(state,policy,printflag = False):
    # recursively play a single round of the poker game until the game is over
    if printflag:
        print "The croupier draws a new card..."
    
    state.drawCard() 
    if printflag:
        state.show()
    if state.gameOver():
        if printflag:
            winner = getWinner(state,True)
        else:
            winner = getWinner(state,False)
       
        if printflag:
            winMessage(winner, state.pot)
    else:
        bet = decideBet(policy,state)
        if printflag:
            print "Player bets %d. House duplicates the bet." % bet
        state.pot += bet*2
        playRound(state, policy, printflag)

    return (state.pot/2)*getWinner(state, printflag)

def getWinner(state, printmode = True):
    # determine the winner of a game that has ended
    p0 = state.player[0]
    p1 = state.player[1]
    h0 = state.house[0]
    h1 = state.house[1]

    PFlush = p0.suit == p1.suit
    HFlush = h0.suit == h1.suit
    PPair = p0.value == p1.value
    HPair = h0.value == h1.value
    PHighcard = max(defValues.index(p0.value), defValues.index(p1.value))
    HHighcard = max(defValues.index(h0.value), defValues.index(h1.value))
    PKicker = min(defValues.index(p0.value), defValues.index(p1.value))
    HKicker = min(defValues.index(h0.value), defValues.index(h1.value))

    if PFlush and not HFlush:
        if printmode: print "Player wins by Flush"
        return 1
    elif HFlush and not PFlush:
        if printmode: print "House wins by Flush"
        return -1
    
    elif PPair and not HPair:
        if printmode: print "Player wins by Pair"
        return 1
    elif HPair and not PPair:
        if printmode: print "House wins by Pair"
        return -1
    elif PHighcard > HHighcard:
        if printmode: print "Player wins by High Card"
        return 1
    elif PHighcard < HHighcard:
        if printmode: print "House wins by High Card"
        return -1
    elif PKicker > HKicker:
        if printmode: print "Player wins by Kicker"
        return 1
    elif PKicker < HKicker:
        if printmode: print "House wins by Kicker"
        return -1
    else:
        return 0

def winMessage(winner, pot):
    # print a win message to the screen
    if winner == 1:
        print "Congratulations, you have won %d dollar!" %pot
    elif winner == 0:
        pot /= 2
        print "Split pot! You receive %d dollar" %pot
    elif winner == -1:
        print "House wins your %d dollar!" %pot


def transitionModel(state, bet):
    # find the transition model for a state and action (i.e. all possible new states with corresponding chance)
    # return a tuple of a list with all possible states and the chance for ending up in these states. 
    possibleStates = []

    if state.gameOver():
        return ([],0)
    
    p = 1/float(len(state.deck))
    
    for i in xrange(len(state.deck)):
        newState = copy.deepcopy(state)
        card = newState.deck[i]
        if len(newState.player) == len(newState.house):
            newState.player.append(card)
        else:
            newState.house.append(card)
            
        newState.deck.remove(card)
        newState.pot += bet*2
        possibleStates.append( newState )
    return (possibleStates, p)

def simple2Full(state):
    # convert simple representation of a state to full
    [player, house, deck, pot] = state
    s = State()
    s.Set(player=player, house=house, deck=deck, pot=pot)
    return s

def deckRemove(deck, removeCard):
    # remove a given card form a given deck
    try:
        for card in deck:
            if card.compare(removeCard):
                deck.remove(card)
    except:
        print "card not in deck"
        raise

def allStates():
    # find all possible states in the game
    # return a list of simple state objects (i.e. lists of [player, house, deck, pot])
    states = []
    
    deck = newDeck()
    ld = len(deck)
    round1states = []
    round2states = []
    round3states = []
    round4states = []
    
    for i in xrange(ld):
        di = [deck[i]]
        iDeck = copy.deepcopy(deck)
        deckRemove(iDeck, deck[i])
        round1states.append([di, [], iDeck])
        for j in xrange(i+1,ld):
            dj = [deck[j]]
            jDeck = copy.deepcopy(iDeck)
            deckRemove(jDeck, deck[j])
            round2states += [[di, dj, jDeck], [dj, di, jDeck]]
            for k in xrange(j+1, ld):
                dk = [deck[k]]
                kDeck = copy.deepcopy(jDeck)
                deckRemove(kDeck, deck[k])
                round3states += [di+dj, dk, kDeck], [di+dk, dj, kDeck], [dk+dj, di, kDeck]
                for l in xrange(k+1, ld):
                    dl = [deck[l]]
                    lDeck = copy.deepcopy(kDeck)
                    deckRemove(lDeck, deck[l])
                    round4states += [di+dj, dk+dl, lDeck], [di+dk, dj+dl, lDeck], [di+dl, dj+dk, lDeck]
                    round4states += [dk+dl, di+dj, lDeck], [dj+dl, di+dk, lDeck], [dj+dk, di+dl, lDeck]

    for s in round1states:
        states.append(s + [2])
    for s in round2states:
        for pot in [2,4]:
            states.append(s + [pot])
    for s in round3states:
        for pot in [2,4,6]:
            states.append(s + [pot])
    for s in round4states:
        for pot in [2,4,6,8]:
            states.append(s + [pot])
    
    return states


def returnValue(newState, bet):
    # Give the immediate reward for a state transition
    reward = -bet
    if newState.gameOver():
        winner = getWinner(newState, False)
        if winner == 1:
            reward += newState.pot
        elif winner == 0:
            reward += newState.pot/2
        elif winner == -1:
            reward = 0
    return reward

def decideBet(policy = 'random', state=None):
    bet = 0
    if policy == 'random':
        bet = random.randint(0,1)
    if policy == 'vpolicy':
        #global V, S, A
        bet = P[toKey(state)]

    if policy == 'mypolicy':    
        bet = myPolicy(state)
    return bet

def actionList(state):
    if len(state.player) + len(state.house) in range(1,3):
        return [0,1]
    else:
        return []

def vpolicy():
    global V,S,A
    Theta = 0.05
    Gamma = 0.9
    Delta = Theta+1

    print "Calculating value function V"
    templength = (len(S)/25)
    while Delta > Theta:
        Delta = 0
        
        for s in xrange(len(S)):
            if (s % templength) == 0:
                print "Percentage done: %.3f" % (float(s)/float(len(S)))
            sKey = toKey(S[s])
            v = V[sKey]

            templist = [0] * len(A) # initialize temporary list of actions with zeroes
            for a in xrange(len(A)):
                PStates,PPoss = transitionModel(simple2Full(S[s]), A[a])
                for PState in PStates:
                    #if len(PState.player)<3 and len(PState.house)<3:
                    templist[a] = templist[a]+PPoss*(returnValue(PState, A[a])+Gamma*V[toKey(PState)])

            V[sKey] = max(templist)
            Delta = max(Delta,abs(v-V[sKey]))
        print Delta
    print "Done"

    # P
    #Gamma = 0.9
    print "Calculating optimal policy P"
    for s in xrange(len(S)):
        templist = [0] * len(A) # initialize actionlist with zeroes   
        for a in xrange(len(A)):
            PStates,PPoss = transitionModel(simple2Full(S[s]), A[a])
            for PState in PStates:
                templist[a] += PPoss*(returnValue(PState, A[a])+Gamma*V[toKey(PState)])

        maxvalue = 0
        maxaction = 0
        for a in xrange(len(A)):
            if templist[a]>maxvalue:
                maxvalue = templist[a]
                maxaction = a
    
        #bet = maxaction
        P[toKey(S[s])] = maxaction
    print "Done"

def initValues():
    # Initialize actions, states and policy, if needed
    S = allStates()
    print "States initialized."
    keys = []
    for s in S:
        keys.append(toKey(s))

    A = [0,1]


    if calcPolicy:
        V = dict(zip(keys, [0]*len(keys)))

    P = dict(zip(keys, [0]*len(keys))) # optimal policy

    if calcPolicy:
        return P,S,V,A
    else:
        return P,S,A

if calcPolicy:
    P,S,V,A = initValues()
else:
    P,S,A = initValues()

if calcPolicy:
    vpolicy()
    fileKObject = open(policyKFile, 'w')
    fileVObject = open(policyVFile, 'w')
    for p,h,po in P.keys():
        
        k = (p,h,po)
        kt = ".".join(p)+"_"+".".join(h)+"_"+str(po)
        fileKObject.write(kt+"\n")
        fileVObject.write(str(P[k])+"\n")
    fileKObject.close()
    fileVObject.close()

else:
    fileKObject = open(policyKFile, 'r')
    fileVObject = open(policyVFile, 'r')
    
    V = dict()    
    for kt,v in zip(fileKObject.readlines(),fileVObject.readlines()):
        p,h,po = kt.split("_")
        k = (p.split("."),h.split("."),int(po))
        s = State()
        for item in k[0]:
            if item != "":
                value,suit = item.split(" of ")
                c = Card(value, suit)
                s.player.append(c)
            
        for item in k[1]:
            if item != "":
                value,suit = item.split(" of ")
                c = Card(value, suit)
                s.house.append(c)
        
        s.pot = k[2]

        for p in s.player:
            deckRemove(s.deck,p)
        for h in s.house:
            deckRemove(s.deck,h)

        V[toKey(s)]=float(v)
    print "Files loaded."
    fileKObject.close()
    fileVObject.close()

def myPolicy(state):
    pcards = len(state.player)

    if pcards == 2:
        p0, p1 = state.player

        PFlush = p0.suit == p1.suit
        PPair = p0.value == p1.value
        PHighcard = max(defValues.index(p0.value), defValues.index(p1.value))
        PKicker = min(defValues.index(p0.value), defValues.index(p1.value))
        HKicker = min(defValues.index(h0.value), defValues.index(h1.value))
    elif pcards == 1:
        [p0] = state.player

    hcards = len(state.house)
    if hcards == 2:
        h0, h1 = state.house
    elif hcards == 1:
        [h0] = state.house    

    if pcards == 1 and hcards == 0 and defValues.index(p0.value) >= 2:
        # player has 1 card, house 0:
        #   bet if card is king or higher
        return 1
    elif pcards == 1 and hcards == 1 and len(state.house)>0:
        # player has 1 card, house 1:
        #   bet if card is higher
        if defValues.index(p0.value) >= defValues.index(h0.value):
            return 1
        
    elif pcards == 2 and hcards == 1:
        # player has 2 cards, house 1:
        #   bet if player has a flush or pair
        #   else: bet if player has a card higher than the house's card
        if PFlush or PPair:
            return 1
        elif PHighcard > defValues.index(h0.value):
            return 1


t = 0
for i in xrange(10000):
    t += newGame('random', False)

print "Average pot (random): %.3f" % (t/float(10000))

t = 0
for i in xrange(10000):
    t += newGame('vpolicy', False)

print "Average pot (vpolicy): %.3f" % (t/float(10000))

#print newGame('vpolicy', True)
#print newGame('vpolicy', True)
#print newGame('vpolicy', True)
#print newGame('vpolicy', True)


t = 0
for i in xrange(10000):
    t += newGame('mypolicy', False)

print "Average pot (mypolicy): %.3f" % (t/float(10000))

#newGame()
#newGame()
#newGame()
#newGame()
#newGame()
#newGame()
#newGame()
